use std::cell::RefCell;
use std::rc::Rc;
use termion::color::{Fg, Red, Reset};

use crate::token::{TextSpan, Token, TokenKind};

const PREFIX_LENGTH: usize = 8;

pub enum DiagnosticKind {
  Error,
  Warning,
}

pub struct Diagnostic {
  pub message: String,
  pub span: TextSpan,
  pub kind: DiagnosticKind,
}

impl Diagnostic {
  pub fn new(message: String, span: TextSpan, kind: DiagnosticKind) -> Self {
    Self {
      message,
      span,
      kind,
    }
  }
}
pub type DiagnosticBagCell = Rc<RefCell<DiagnosticBag>>;

pub struct DiagnosticBag {
  pub diagnostics: Vec<Diagnostic>,
}

impl DiagnosticBag {
  pub fn new() -> Self {
    Self {
      diagnostics: vec![],
    }
  }

  pub fn report_error(&mut self, message: String, span: TextSpan) {
    let error = Diagnostic::new(message, span, DiagnosticKind::Error);
    self.diagnostics.push(error);
  }

  pub fn report_warning(&mut self, message: String, span: TextSpan) {
    let warning = Diagnostic::new(message, span, DiagnosticKind::Warning);
    self.diagnostics.push(warning);
  }

  pub fn report_unexpected_token(&mut self, expected: &TokenKind, token: &Token) {
    self.report_error(
      format!("expected <{}>, found <{}>", expected, token.kind),
      token.span.clone(),
    );
  }

  pub fn report_expected_expression(&mut self, token: &Token) {
    self.report_error(
      format!("Expected expression, found <{}>", token.kind),
      token.span.clone(),
    )
  }
}

pub struct SourceText {
  text: String,
}

impl SourceText {
  pub fn new(text: String) -> Self {
    Self { text }
  }

  pub fn line_index(&self, position: usize) -> usize {
    self.text[..=position].lines().count() - 1
  }

  pub fn get_line(&self, index: usize) -> &str {
    self.text.lines().nth(index).unwrap()
  }

  pub fn line_start(&self, index: usize) -> usize {
    self
      .text
      .lines()
      .take(index)
      .map(|line| line.len() + 1)
      .sum()
  }
}

pub struct DiagnosticsPrinter<'a> {
  text: &'a SourceText,
  diagnostics: &'a [Diagnostic],
}

impl<'a> DiagnosticsPrinter<'a> {
  pub fn new(text: &'a SourceText, diagnostics: &'a [Diagnostic]) -> Self {
    Self { text, diagnostics }
  }

  /// Stringfies the diagnostic.
  ///
  /// It uses the following format:
  ///
  /// let <red>x<reset> = 6;
  ///         ^
  ///         |
  ///         +-- This is the error message (<line>:<column>)
  pub fn stringify_diagnostic(&self, diagnostic: &Diagnostic) -> String {
    let line_index = self.text.line_index(diagnostic.span.start);
    let line = self.text.get_line(line_index);
    let line_start = self.text.line_start(line_index);

    let column = diagnostic.span.start - line_start;

    let (prefix, span, suffix) = self.get_text_spans(diagnostic, line, column);

    let indent = std::cmp::min(PREFIX_LENGTH, column);
    let (arrow_pointers, arrow_line) = Self::format_arrow(diagnostic, indent);

    let error_message = Self::format_error_message(diagnostic, indent, column, line_index);

    format!(
      "{}{}{}{}{}\n{}\n{}\n{}",
      prefix,
      Fg(Red),
      span,
      Fg(Reset),
      suffix,
      arrow_pointers,
      arrow_line,
      error_message
    )
  }

  fn get_text_spans(
    &self,
    diagnostic: &Diagnostic,
    line: &'a str,
    column: usize,
  ) -> (&str, &str, &str) {
    // column, PREFIX_LENGTH 转成 isize 在计算
    let prefix_start = std::cmp::max(0, column as isize - PREFIX_LENGTH as isize) as usize;
    let prefix_end = column;
    let suffix_start = std::cmp::min(column + diagnostic.span.length(), line.len());
    let suffix_end = std::cmp::min(suffix_start + PREFIX_LENGTH, line.len());

    let prefix = &line[prefix_start..prefix_end];
    let span = &line[prefix_end..suffix_start];
    let suffix = &line[suffix_start..suffix_end];

    (prefix, span, suffix)
  }

  fn format_arrow(diagnostic: &Diagnostic, indent: usize) -> (String, String) {
    let arrow_pointers = format!(
      "{:indent$}{}",
      "",
      // std::iter::repeat('^').take(diagnostic.span.length()).
      // collect::<String>(),
      "^".repeat(diagnostic.span.length()),
      indent = indent
    );

    let arrow_line = format!("{:indent$}|", "", indent = indent);

    (arrow_pointers, arrow_line)
  }

  fn format_error_message(
    diagnostic: &Diagnostic,
    indent: usize,
    column: usize,
    line_index: usize,
  ) -> String {
    format!(
      "{:indent$}+-- {} ({}:{})",
      "",
      diagnostic.message,
      column + 1,
      line_index + 1,
      indent = indent,
    )
  }

  pub fn print(&self) {
    for diagnostic in self.diagnostics {
      println!("{}", self.stringify_diagnostic(diagnostic));
    }
  }
}
